cover

长安链 Chainmaker 体验记录

本文主要记录了在体验长安链 Chainmaker 过程中遇到的一些问题。如:单机部署管理后台和区块链浏览器;修复 InsertBlockAndTx 卡死;Node.js SDK 不支持国密。记录了针对这些问题的分析以及一些可以尝试的解决思路。

2023-09-11

本次体验长安链 Chainmaker 的过程中主要涉及到了以下几个部分的内容:

  1. 部署管理后台区块链浏览器
  2. 使用管理后台部署区块链
  3. 使用管理后台部署智能合约
  4. 使用管理后台调用智能合约
  5. 使用区块链浏览器查看 Transaction
  6. 使用 sdk 调用智能合约

使用的长安链版本为 v2.3.0,所有操作均按照官方文档中的步骤进行。在体验过程中,问题主要出现在第一、第三和第六上,具体的问题会在后面说明。

后续的内容分成主要分成三个部分:第一部分介绍体验过程中遇到的三个问题;第二部分提供单机部署管理后台和区块链浏览器的方法;第三部分简单介绍了如何实现一个简单的 go-sdk API 服务。

相关代码已上传 Github

heading

遇到的问题

按照官方文档的操作步骤基本上都能够达到预期的目标,但是在体验过程中还是遇到了一下无法在文档及问题列表中找到解决方案的问题,后续内容主要是对这些问题进行介绍。

heading

问题一、单机部署管理后台和区块链浏览器

原本计划是直接使用长安链官方提供管理后台和区块链浏览器的 docker-compose 启动服务,并在宿主机上使用 Nginx 通过子域名反向代理到对应的服务。但是部署完成之后发现两个子域名都只能访问到管理后台应用。

通过查看 management-webexplorer-web 的 Dockerfile 后发现,这两个 web 应用镜像在容器内部已经使用了 Nginx 作为 web 应用服务器,并且 management-web 的 docker-compose 的中的端口映射是

80:80
,所以在服务启动之后外部的访问 80 端口时实际上是访问到的是容器内的部的 Nginx 服务。

为了能够使用宿主机的 Ningx 代理管理后台和区块链浏览器,需要修改管理后台服务的端口映射,因此将官方提供的 managementexplorer 的 docker-compose 文件进行合并,并将

cm_mgmt_server
的端口转发设置为
9995:80

具体实现可以参见后文

heading

问题二、管理后台无法订阅部署了智能合约的区块链

使用管理后台生成部署材料并进行部署,并在管理后台订阅了区块链网络。但在提交部署智能合约的时候出现了管理后台服务长时间无法访问,然后自动重启的问题。

查看日志后发现管理后台后端在调用插入

InsertBlockAndTx
的时候卡住了。通过在本地进行调试后发现,当管理后台后端向数据库中插入
Transaction
的时候会卡死。通过修改源码(
src/sync/resolver.go
)进行调试,发现如果使用了
transaction.ContractParams
字段,则后端应用就会直接卡死。似乎是在
json.unmarshal
的时候出现了问题,但由于对 go 语言并不熟悉,在这里直接用最简单的的方法 - 将
Transaction.ContractParams
相关的代码直接注释掉来回避这个问题,具体内容见后文

heading

问题三、Node.js 的 SDK 不支持国密 SSL

尝试使用长安链提供的 Node.js SDK 调用智能合约,经测试后发现如果使用国密证书进行调用会提示证书不支持(unsupported)。查询后发现目前只有 go-sdk 和 java-sdk 实现了国密 SSL

为了使用国密所以选择使用

实现一个用于调用智能合约的 API 服务

heading

部署管理后台和区块链浏览器

服务器运行环境:

  1. Debian@12;
  2. 安装了 docker 及 docker-compose;

ssh 到服务器中并初始化工程目录:

mkdir -p /src/chainmaker mkdir -p /data/chainmaker/{management,explorer}
heading

初始化项目

在本地工作目录中按以下的脚本初始化一个工程项目:

# 创建项目目录并初始化 mkdir chainmaker && cd chainmaker && git init # 以 v2.3.0 为例 # 将 management-backend 作为 submodule 以便避免"问题二" git submodule add https://git.chainmaker.org.cn/chainmaker/management-backend.git management-backend pushd management-backend git checkout tags/$VERSION -b $VERSION popd # 新建两个目录用于保存数据库和后端配置 mkdir {management_configs,explorer_configs}
heading

避免"问题二"

注释掉

management-backend/src/sync/resolver.go
中第 124 行到 129 行的内容以避免上问中提到的问题二。

修改之后的问题是在管理后台的区块链浏览器中无法查看调用合约时的参数,但如果部署了区块链浏览器其实并不是一个很大的问题。同时在调试中发现,只有部署或升级智能合约时的

transaction.ContractParams
会导致应用卡死,因此也可以添加条件语句实现针对性的忽略。

heading

更新默认配置

management_configs
中添加
.env
文件,用于配置管理后台数据库:

MYSQL_ROOT_PASSWORD=<管理后台 Mysql Root Password> MYSQL_USER=chainmaker MYSQL_PASSWORD=<chainmaker_mgmt 密码> MYSQL_DATABASE=chainmaker_mgmt MYSQL_TCP_PORT=3306
management_configs.env

在同一个文件夹下添加

config.yml
文件供管理后台后端服务使用:

web: address: 0.0.0.0 port: 9999 cross_domain: true session_age: 86400 captcha: height: 80 width: 200 noise_count: 5 length: 4 # 错误提示语言,取值:0 - 英文,1 - 中文 errmsg_lang: 1 # 加载链信息间隔时间,单位:秒 load_period_seconds : 60 # 默认 admin 用户密码,和重置用户密码 password: <管理后台密码> # 日志 agent 的端口 agent_port: 22301 # 上报日志链接 report_url: https://bugreport.chainmaker.org.cn/v1/reportLogs db: host: cm_db port: 3306 database: chainmaker_mgmt user: chainmaker passwd: <chainmaker_mgmt 密码>
management_configs/config.yml

对区块链浏览器的配置同理,在

explorer_configs
中添加
.env
config.yml
文件:

MYSQL_ROOT_PASSWORD=<区块链浏览器 Mysql Root Password> MYSQL_USER=chainmaker MYSQL_PASSWORD=<chainmaker_explorer 密码> MYSQL_DATABASE=chainmaker_explorer MYSQL_TCP_PORT=3306
explore_configs.env
web: address: 0.0.0.0 port: 9997 cross_domain: true node: # 链和节点更新时间 update_time: 30 # 节点断开连接时间和新增链时间 sync_time: 30 chain: show_config: true db: host: cm_explorer_db port: 3307 database: chainmaker_explorer user: chainmaker passwd: <chainmaker_explorer 密码>
explorer_configs/config.yml
heading

使用 docker-compose 运行服务

添加

docker-compose.yml
文件,并填写如下内容:

version: "3.9" services: cm_db: container_name: "cm_db" image: mysql:5.7 volumes: - /data/chainmaker/mgmt:/var/lib/mysql restart: always env_file: - management_configs/.env command: [ "mysqld", "--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci", "--max_allowed_packet=200M", ] cm_mgmt_server: container_name: "cm_mgmt_server" depends_on: - cm_db build: context: management-backend dockerfile: Dockerfile image: management-backend:v2.3.0 volumes: - /src/chainmaker/management_configs:/chainmaker-management/configs ports: - "9999:9999" restart: always cm_mgmt_web: container_name: "cm_mgmt_web" depends_on: - cm_mgmt_server image: chainmakerofficial/management-web:v2.3.0 ports: - "9995:80" restart: always cm_explorer_db: container_name: "cm_explorer_db" image: mysql:5.7 volumes: - /data/chainmaker/explorer:/var/lib/mysql restart: always env_file: - explorer_configs/.env command: [ "mysqld", "--character-set-server=utf8mb4", "--collation-server=utf8mb4_unicode_ci", "--max_allowed_packet=200M", ] cm_explorer_server: container_name: "cm_explorer_server" image: chainmakerofficial/explorer-backend:v2.3.0 volumes: - /src/chainmaker/explorer_configs:/chainmaker-explorer-backend/configs depends_on: - cm_explorer_db ports: - "9997:9997" environment: show_config: true restart: always cm_explorer_web: container_name: "cm_explorer_web" depends_on: - cm_explorer_server image: chainmakerofficial/explorer-web:v2.3.0 ports: - "9996:8080" restart: always
docker-compose.yaml
heading

使用 Nginx 代理前端应用

使用以下的 nginx 配置文件分别代理管理后台和区块链浏览器:

server { listen 80; server_name management.chainmaker-example.com; location / { proxy_pass http://127.0.0.1:9995; } location /chainmaker { proxy_read_timeout 300; proxy_pass http://127.0.0.1:9999; client_max_body_size 0; } } server { listen 80; server_name explorer.chainmaker-example.com; location / { proxy_pass http://127.0.0.1:9996; } location /chainmaker/ { proxy_read_timeout 300; proxy_pass http://127.0.0.1:9997/chainmaker; } location /signatures/ { proxy_read_timeout 300; proxy_pass http://127.0.0.1:9996/; } }
chainmaker.conf
heading

添加部署脚本

添加运行服务的脚本:

cd /src/chainmaker docker compose down && docker compose up -d --build ln -sf /src/chainmaker/chainmaker.conf /etc/nginx/sites-enabled/chainmaker.conf nginx -t && nginx -s reload
.scripts/start.sh

添加部署脚本:

REMOTE=$1 # 初始化文件夹 ssh $REMOTE -t "mkdir -p /src/chainmaker && /data/chainmaker/{management, explorer}" rsync -av . $REMOTE:/src/chainmaker \ --exclude=.DS_Store \ --exclude=.git \ --exclude=build \ --exclude=node_modules ssh $REMOTE -t < .scripts/start.sh
.scripts/deploy.sh

部署:

bash .scripts/deploy.sh <user@host>
heading

实现一个简单的 go-sdk API 服务

由于上文提到的"问题三",因此需要使用

go-sdk
实现一个用于调用智能合约的 API 服务。

首先初始化一个 go 项目,并添加依赖:

go get -u chainmaker.org/chainmaker/sdk-go/v2@v2.3.0 go get -u github.com/gin-gonic/gin

main.go
中添加以下代码:

package main import ( "fmt" "os" "chainmaker.org/chainmaker/pb-go/v2/common" sdk "chainmaker.org/chainmaker/sdk-go/v2" "github.com/gin-gonic/gin" ) const CONFIG_PATH = "/configs/sdk_config.yml" func InitSdkClient() *sdk.ChainClient { client, err := sdk.NewChainClient(sdk.WithConfPath(CONFIG_PATH)) if err != nil { fmt.Printf("Initialize sdk failed: %s", err) os.Exit(1) return nil } fmt.Printf("Client initialized\n") return client } func CallUserContract( client *sdk.ChainClient, action string, contractName string, method string, params map[string]string, ) (int, map[string]string) { kv_pairs := buildParamPairs(params) var resp *common.TxResponse var err error if action == "invoke" { resp, err = client.InvokeContract( contractName, method, "", kv_pairs, -1, true, // sync ) } else { resp, err = client.QueryContract( contractName, method, kv_pairs, -1, ) } if err != nil { fmt.Printf("[ERROR] Invoke contract failed: %s", err.Error()) return 502, map[string]string{ "message": err.Error(), } } if resp.Code != common.TxStatusCode_SUCCESS { return 400, map[string]string{ "message": resp.ContractResult.Message, } } return 200, map[string]string{ "message": resp.Message, "data": string(resp.ContractResult.Result), } } func buildParamPairs(params map[string]string) []*common.KeyValuePair { var kv_pairs []*common.KeyValuePair for k := range params { kv_pairs = append(kv_pairs, &common.KeyValuePair{ Key: k, Value: []byte(params[k]), }) } return kv_pairs } type CallContractParamsDto struct { Action string `json:action` ContractName string `json:contractName` Method string `json:method` Params map[string]string `json:params` } func StartApi(client *sdk.ChainClient) { router := gin.Default() router.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{"message": "Hello, Gin!"}) }) router.POST("/contract", func(c *gin.Context) { var dto CallContractParamsDto if err := c.BindJSON(&dto); err != nil { c.JSON(400, gin.H{"message": "Invalid Body"}) return } code, data := CallUserContract(client, dto.Action, dto.ContractName, dto.Method, dto.Params) c.JSON(code, data) }) fmt.Println("Listening on port :8080") router.Run(":8080") } func main() { client := InitSdkClient() StartApi(client) }
main.go

创建一个 Dockerfile 文件用于部署 sdk 服务。

FROM golang:1.21-bookworm as build ENV GOPROXY=https://goproxy.cn,direct WORKDIR /app COPY . . RUN go mod tidy && go build -o /app/bin/sdk.bin FROM debian:bookworm EXPOSE 8080 WORKDIR /app COPY --from=build /app/bin /app ENTRYPOINT ["/app/sdk.bin", "/configs/sdk_config.yml"]
sdk/Dockerfile

编译镜像:

docker build . -t chainmaker-sdk:v2.3.0

通过管理后台中的

区块链管理/区块链概览/下载链配置
下载区块链配置文件。假设将配置文件解压到
/root/configs
中,使用编辑器将
/root/configs/sdk\_config.yml
文件中的
./crypto-config
全部替换为
/configs/crypto-config
,然后使用下面的脚本启动容器:

docker run \ -p 8080:8080 \ -v /root/configs:/configs \ -d chainmaker-sdk chainmaker-sdk:v2.3.0

接下来尝试使用 sdk 接口调用智能合约:

curl -X "POST" "http://127.0.0.1:8080/contract" \ -H 'Content-Type: application/json; charset=utf-8' \ -d $'{ "action": "invoke", "method": "increase", "contractName": "counter" }' curl -X "POST" "http://127.0.0.1:8080/contract" \ -H 'Content-Type: application/json; charset=utf-8' \ -d $'{ "action": "query", "method": "query", "contractName": "counter" }'

如果得到的响应为:

{"message":"send QUERY_CONTRACT failed, all client connections are busy"}

则可能是

sdk_config.yml
中的
nodes.tls_host_name
的配置问题。如果使用的是自签发的证书,则确认
nodes.node_addr
nodes.tls_host_name
是否正确。如果使用的是管理后台生成的证书,则可以尝试将
nodes.tls_host_name
修改为
localhost
后再重试。